// Mathieu Jacomy @ Sciences Po Médialab & WebAtlas
// (requires sigma.js to be loaded)
sigma.forceatlas2 = sigma.forceatlas2 || {};
sigma.forceatlas2.ForceAtlas2 = function(graph) {
	sigma.classes.Cascade.call(this);
	var self = this;
	this.graph = graph;

	this.p = {
		linLogMode : false,
		outboundAttractionDistribution : false,
		adjustSizes : false,
		edgeWeightInfluence : 0,
		scalingRatio : 1,
		strongGravityMode : false,
		gravity : 1,
		jitterTolerance : 1,
		barnesHutOptimize : false,
		barnesHutTheta : 1.2,
		speed : 1,
		outboundAttCompensation : 1,
		totalSwinging : 0,
		totalEffectiveTraction : 0,
		complexIntervals : 500,
		simpleIntervals : 1000
	};

	// The state tracked from one atomic "go" to another
	this.state = {
		step : 0,
		index : 0
	};
	this.rootRegion

	// Runtime (the ForceAtlas2 itself)
	this.init = function() {
		self.state = {
			step : 0,
			index : 0
		};

		self.graph.nodes.forEach(function(n) {
			n.fa2 = {
				mass : 1 + n.degree,
				old_dx : 0,
				old_dy : 0,
				dx : 0,
				dy : 0
			};
		});

		return self;
	}

	this.go = function() {
		while(self.atomicGo()) {
		}
	}

	this.atomicGo = function() {
		var graph = self.graph;
		var nodes = graph.nodes;
		var edges = graph.edges;

		var cInt = self.p.complexIntervals;
		var sInt = self.p.simpleIntervals;

		switch (self.state.step) {
			case 0:
				// Pass init
				// Initialise layout data
				nodes.forEach(function(n) {
					if(n.fa2) {
						n.fa2.mass = 1 + n.degree;
						n.fa2.old_dx = n.fa2.dx;
						n.fa2.old_dy = n.fa2.dx;
						n.fa2.dx = 0;
						n.fa2.dy = 0;
					} else {
						n.fa2 = {
							mass : 1 + n.degree,
							old_dx : 0,
							old_dy : 0,
							dx : 0,
							dy : 0
						};
					}
				});

				// If Barnes Hut active, initialize root region
				if(self.p.barnesHutOptimize) {
					self.rootRegion = new sigma.forceatlas2.Region(nodes, 0);
					self.rootRegion.buildSubRegions();
				}

				// If outboundAttractionDistribution active, compensate.
				if(self.p.outboundAttractionDistribution) {
					self.p.outboundAttCompensation = 0;
					nodes.forEach(function(n) {
						self.p.outboundAttCompensation += n.fa2.mass;
					});
					self.p.outboundAttCompensation /= nodes.length;
				}
				self.state.step = 1;
				self.state.index = 0;
				return true;
				break;

			case 1:
				// Repulsion
				var Repulsion = self.ForceFactory.buildRepulsion(self.p.adjustSizes, self.p.scalingRatio);

				if(self.p.barnesHutOptimize) {
					var rootRegion = self.rootRegion;

					// Pass to the scope of forEach
					var barnesHutTheta = self.p.barnesHutTheta;
					var i = self.state.index;
					while(i < nodes.length && i < self.state.index + cInt) {
						var n = nodes[i++];
						if(n.fa2)
							rootRegion.applyForce(n, Repulsion, barnesHutTheta);
					}
					if(i == nodes.length) {
						self.state.step = 2;
						self.state.index = 0;
					} else {
						self.state.index = i;
					}
				} else {
					var i1 = self.state.index;
					while(i1 < nodes.length && i1 < self.state.index + cInt) {
						var n1 = nodes[i1++];
						if(n1.fa2)
							nodes.forEach(function(n2, i2) {
								if(i2 < i1 && n2.fa2) {
									Repulsion.apply_nn(n1, n2);
								}
							});
					}
					if(i1 == nodes.length) {
						self.state.step = 2;
						self.state.index = 0;
					} else {
						self.state.index = i1;
					}
				}
				return true;
				break;

			case 2:
				// Gravity
				var Gravity = (self.p.strongGravityMode) ? (self.ForceFactory.getStrongGravity(self.p.scalingRatio)) : (self.ForceFactory.buildRepulsion(self.p.adjustSizes, self.p.scalingRatio));
				// Pass gravity and scalingRatio to the scope of the function
				var gravity = self.p.gravity, scalingRatio = self.p.scalingRatio;

				var i = self.state.index;
				while(i < nodes.length && i < self.state.index + sInt) {
					var n = nodes[i++];
					if(n.fa2)
						Gravity.apply_g(n, gravity / scalingRatio);
				}

				if(i == nodes.length) {
					self.state.step = 3;
					self.state.index = 0;
				} else {
					self.state.index = i;
				}
				return true;
				break;

			case 3:
				// Attraction
				var Attraction = self.ForceFactory.buildAttraction(self.p.linLogMode, self.p.outboundAttractionDistribution, self.p.adjustSizes, 1 * ((self.p.outboundAttractionDistribution) ? (self.p.outboundAttCompensation) : (1)));

				var i = self.state.index;
				if(self.p.edgeWeightInfluence == 0) {
					while(i < edges.length && i < self.state.index + cInt) {
						var e = edges[i++];
						Attraction.apply_nn(e.source, e.target, 1);
					}
				} else if(self.p.edgeWeightInfluence == 1) {
					while(i < edges.length && i < self.state.index + cInt) {
						var e = edges[i++];
						Attraction.apply_nn(e.source, e.target, e.weight || 1);
					}
				} else {
					while(i < edges.length && i < self.state.index + cInt) {
						var e = edges[i++];
						Attraction.apply_nn(e.source, e.target, Math.pow(e.weight || 1, self.p.edgeWeightInfluence));
					}
				}

				if(i == edges.length) {
					self.state.step = 4;
					self.state.index = 0;
				} else {
					self.state.index = i;
				}

				return true;
				break;

			case 4:
				// Auto adjust speed
				var totalSwinging = 0;
				// How much irregular movement
				var totalEffectiveTraction = 0;
				// Hom much useful movement

				nodes.forEach(function(n) {
					var fixed = n.fixed || false;
					if(!fixed && n.fa2) {
						var swinging = Math.sqrt(Math.pow(n.fa2.old_dx - n.fa2.dx, 2) + Math.pow(n.fa2.old_dy - n.fa2.dy, 2));

						// If the node has a burst change of direction, then it's not converging.
						totalSwinging += n.fa2.mass * swinging;
						totalEffectiveTraction += n.fa2.mass * 0.5 * Math.sqrt(Math.pow(n.fa2.old_dx + n.fa2.dx, 2) + Math.pow(n.fa2.old_dy + n.fa2.dy, 2));
					}
				});

				self.p.totalSwinging = totalSwinging;
				self.p.totalEffectiveTraction = totalEffectiveTraction;

				// We want that swingingMovement < tolerance * convergenceMovement
				var targetSpeed = Math.pow(self.p.jitterTolerance, 2) * self.p.totalEffectiveTraction / self.p.totalSwinging;

				// But the speed shoudn't rise too much too quickly, since it would make the convergence drop dramatically.
				var maxRise = 0.5;
				// Max rise: 50%
				self.p.speed = self.p.speed + Math.min(targetSpeed - self.p.speed, maxRise * self.p.speed);

				// Save old coordinates
				nodes.forEach(function(n) {
					n.old_x = +n.x;
					n.old_y = +n.y;
				});

				self.state.step = 5;
				return true;
				break;

			case 5:
				// Apply forces
				var i = self.state.index;
				if(self.p.adjustSizes) {
					var speed = self.p.speed;
					// If nodes overlap prevention is active,
					// it's not possible to trust the swinging mesure.
					while(i < nodes.length && i < self.state.index + sInt) {
						var n = nodes[i++];
						var fixed = n.fixed || false;
						if(!fixed && n.fa2) {
							// Adaptive auto-speed: the speed of each node is lowered
							// when the node swings.
							var swinging = Math.sqrt((n.fa2.old_dx - n.fa2.dx) * (n.fa2.old_dx - n.fa2.dx) + (n.fa2.old_dy - n.fa2.dy) * (n.fa2.old_dy - n.fa2.dy));
							var factor = 0.1 * speed / (1 + speed * Math.sqrt(swinging));

							var df = Math.sqrt(Math.pow(n.fa2.dx, 2) + Math.pow(n.fa2.dy, 2));

							factor = Math.min(factor * df, 10) / df;

							n.x += n.fa2.dx * factor;
							n.y += n.fa2.dy * factor;
						}
					}
				} else {
					var speed = self.p.speed;
					while(i < nodes.length && i < self.state.index + sInt) {
						var n = nodes[i++];
						var fixed = n.fixed || false;
						if(!fixed && n.fa2) {
							// Adaptive auto-speed: the speed of each node is lowered
							// when the node swings.
							var swinging = Math.sqrt((n.fa2.old_dx - n.fa2.dx) * (n.fa2.old_dx - n.fa2.dx) + (n.fa2.old_dy - n.fa2.dy) * (n.fa2.old_dy - n.fa2.dy));
							var factor = speed / (1 + speed * Math.sqrt(swinging));

							n.x += n.fa2.dx * factor;
							n.y += n.fa2.dy * factor;
						}
					}
				}

				if(i == nodes.length) {
					self.state.step = 0;
					self.state.index = 0;
					return false;
				} else {
					self.state.index = i;
					return true;
				}
				break;

			default:
				throw new Error('ForceAtlas2 - atomic state error');
				break;
		}
	}

	this.end = function() {
		this.graph.nodes.forEach(function(n) {
			n.fa2 = null;
		});
	}
	// Auto Settings
	this.setAutoSettings = function() {
		var graph = this.graph;

		// Tuning
		if(graph.nodes.length >= 100) {
			this.p.scalingRatio = 2.0;
		} else {
			this.p.scalingRatio = 10.0;
		}
		this.p.strongGravityMode = false;
		this.p.gravity = 1;

		// Behavior
		this.p.outboundAttractionDistribution = false;
		this.p.linLogMode = false;
		this.p.adjustSizes = false;
		this.p.edgeWeightInfluence = 1;

		// Performance
		if(graph.nodes.length >= 50000) {
			this.p.jitterTolerance = 10;
		} else if(graph.nodes.length >= 5000) {
			this.p.jitterTolerance = 1;
		} else {
			this.p.jitterTolerance = 0.1;
		}
		if(graph.nodes.length >= 1000) {
			this.p.barnesHutOptimize = true;
		} else {
			this.p.barnesHutOptimize = false;
		}
		this.p.barnesHutTheta = 1.2;

		return this;
	}
	// All the different forces
	this.ForceFactory = {
		buildRepulsion : function(adjustBySize, coefficient) {
			if(adjustBySize) {
				return new this.linRepulsion_antiCollision(coefficient);
			} else {
				return new this.linRepulsion(coefficient);
			}
		},

		getStrongGravity : function(coefficient) {
			return new this.strongGravity(coefficient);
		},

		buildAttraction : function(logAttr, distributedAttr, adjustBySize, c) {
			if(adjustBySize) {
				if(logAttr) {
					if(distributedAttr) {
						return new this.logAttraction_degreeDistributed_antiCollision(c);
					} else {
						return new this.logAttraction_antiCollision(c);
					}
				} else {
					if(distributedAttr) {
						return new this.linAttraction_degreeDistributed_antiCollision(c);
					} else {
						return new this.linAttraction_antiCollision(c);
					}
				}
			} else {
				if(logAttr) {
					if(distributedAttr) {
						return new this.logAttraction_degreeDistributed(c);
					} else {
						return new this.logAttraction(c);
					}
				} else {
					if(distributedAttr) {
						return new this.linAttraction_massDistributed(c);
					} else {
						return new this.linAttraction(c);
					}
				}
			}
		},

		// Repulsion force: Linear
		linRepulsion : function(c) {
			this.coefficient = c;
			this.apply_nn = function(n1, n2) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;
					var distance = Math.sqrt(xDist * xDist + yDist * yDist);

					if(distance > 0) {
						// NB: factor = force / distance
						var factor = this.coefficient * n1.fa2.mass * n2.fa2.mass / Math.pow(distance, 2);

						n1.fa2.dx += xDist * factor;
						n1.fa2.dy += yDist * factor;

						n2.fa2.dx -= xDist * factor;
						n2.fa2.dy -= yDist * factor;
					}
				}
			}

			this.apply_nr = function(n, r) {
				// Get the distance
				var xDist = n.x - r.config('massCenterX');
				var yDist = n.y - r.config('massCenterY');
				var distance = Math.sqrt(xDist * xDist + yDist * yDist);

				if(distance > 0) {
					// NB: factor = force / distance
					var factor = this.coefficient * n.fa2.mass * r.config('mass') / Math.pow(distance, 2);

					n.fa2.dx += xDist * factor;
					n.fa2.dy += yDist * factor;
				}
			}

			this.apply_g = function(n, g) {
				// Get the distance
				var xDist = n.x;
				var yDist = n.y;
				var distance = Math.sqrt(xDist * xDist + yDist * yDist);

				if(distance > 0) {
					// NB: factor = force / distance
					var factor = this.coefficient * n.fa2.mass * g / distance;

					n.fa2.dx -= xDist * factor;
					n.fa2.dy -= yDist * factor;
				}
			}
		},

		linRepulsion_antiCollision : function(c) {
			this.coefficient = c;
			this.apply_nn = function(n1, n2) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;
					var distance = Math.sqrt(xDist * xDist + yDist * yDist) - n1.size - n2.size;

					if(distance > 0) {
						// NB: factor = force / distance
						var factor = this.coefficient * n1.fa2.mass * n2.fa2.mass / Math.pow(distance, 2);

						n1.fa2.dx += xDist * factor;
						n1.fa2.dy += yDist * factor;

						n2.fa2.dx -= xDist * factor;
						n2.fa2.dy -= yDist * factor;

					} else if(distance < 0) {
						var factor = 100 * this.coefficient * n1.fa2.mass * n2.fa2.mass;

						n1.fa2.dx += xDist * factor;
						n1.fa2.dy += yDist * factor;

						n2.fa2.dx -= xDist * factor;
						n2.fa2.dy -= yDist * factor;
					}
				}
			}

			this.apply_nr = function(n, r) {
				// Get the distance
				var xDist = n.fa2.x() - r.getMassCenterX();
				var yDist = n.fa2.y() - r.getMassCenterY();
				var distance = Math.sqrt(xDist * xDist + yDist * yDist);

				if(distance > 0) {
					// NB: factor = force / distance
					var factor = this.coefficient * n.fa2.mass * r.getMass() / Math.pow(distance, 2);

					n.fa2.dx += xDist * factor;
					n.fa2.dy += yDist * factor;
				} else if(distance < 0) {
					var factor = -this.coefficient * n.fa2.mass * r.getMass() / distance;

					n.fa2.dx += xDist * factor;
					n.fa2.dy += yDist * factor;
				}
			}

			this.apply_g = function(n, g) {
				// Get the distance
				var xDist = n.x;
				var yDist = n.y;
				var distance = Math.sqrt(xDist * xDist + yDist * yDist);

				if(distance > 0) {
					// NB: factor = force / distance
					var factor = this.coefficient * n.fa2.mass * g / distance;

					n.fa2.dx -= xDist * factor;
					n.fa2.dy -= yDist * factor;
				}
			}
		},

		// Repulsion force: Strong Gravity (as a Repulsion Force because it is easier)
		strongGravity : function(c) {
			this.coefficient = c;
			this.apply_nn = function(n1, n2) {
				// Not Relevant
			}
			this.apply_nr = function(n, r) {
				// Not Relevant
			}

			this.apply_g = function(n, g) {
				// Get the distance
				var xDist = n.x;
				var yDist = n.y;
				var distance = Math.sqrt(xDist * xDist + yDist * yDist);

				if(distance > 0) {
					// NB: factor = force / distance
					var factor = this.coefficient * n.fa2.mass * g;

					n.fa2.dx -= xDist * factor;
					n.fa2.dy -= yDist * factor;
				}
			}
		},

		// Attraction force: Linear
		linAttraction : function(c) {
			this.coefficient = c;

			this.apply_nn = function(n1, n2, e) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;

					// NB: factor = force / distance
					var factor = -this.coefficient * e;

					n1.fa2.dx += xDist * factor;
					n1.fa2.dy += yDist * factor;

					n2.fa2.dx -= xDist * factor;
					n2.fa2.dy -= yDist * factor;
				}
			}
		},

		// Attraction force: Linear, distributed by mass (typically, degree)
		linAttraction_massDistributed : function(c) {
			this.coefficient = c;

			this.apply_nn = function(n1, n2, e) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;

					// NB: factor = force / distance
					var factor = -this.coefficient * e / n1.fa2.mass;

					n1.fa2.dx += xDist * factor;
					n1.fa2.dy += yDist * factor;

					n2.fa2.dx -= xDist * factor;
					n2.fa2.dy -= yDist * factor;
				}
			}
		},

		// Attraction force: Logarithmic
		logAttraction : function(c) {
			this.coefficient = c;

			this.apply_nn = function(n1, n2, e) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;
					var distance = Math.sqrt(xDist * xDist + yDist * yDist);

					if(distance > 0) {
						// NB: factor = force / distance
						var factor = -this.coefficient * e * Math.log(1 + distance) / distance;

						n1.fa2.dx += xDist * factor;
						n1.fa2.dy += yDist * factor;

						n2.fa2.dx -= xDist * factor;
						n2.fa2.dy -= yDist * factor;
					}
				}
			}
		},

		// Attraction force: Linear, distributed by Degree
		logAttraction_degreeDistributed : function(c) {
			this.coefficient = c;

			this.apply_nn = function(n1, n2, e) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;
					var distance = Math.sqrt(xDist * xDist + yDist * yDist);

					if(distance > 0) {
						// NB: factor = force / distance
						var factor = -this.coefficient * e * Math.log(1 + distance) / distance / n1.fa2.mass;

						n1.fa2.dx += xDist * factor;
						n1.fa2.dy += yDist * factor;

						n2.fa2.dx -= xDist * factor;
						n2.fa2.dy -= yDist * factor;
					}
				}
			}
		},

		// Attraction force: Linear, with Anti-Collision
		linAttraction_antiCollision : function(c) {
			this.coefficient = c;

			this.apply_nn = function(n1, n2, e) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;
					var distance = Math.sqrt(xDist * xDist + yDist * yDist);

					if(distance > 0) {
						// NB: factor = force / distance
						var factor = -this.coefficient * e;

						n1.fa2.dx += xDist * factor;
						n1.fa2.dy += yDist * factor;

						n2.fa2.dx -= xDist * factor;
						n2.fa2.dy -= yDist * factor;
					}
				}
			}
		},

		// Attraction force: Linear, distributed by Degree, with Anti-Collision
		linAttraction_degreeDistributed_antiCollision : function(c) {
			this.coefficient = c;

			this.apply_nn = function(n1, n2, e) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;
					var distance = Math.sqrt(xDist * xDist + yDist * yDist);

					if(distance > 0) {
						// NB: factor = force / distance
						var factor = -this.coefficient * e / n1.fa2.mass;

						n1.fa2.dx += xDist * factor;
						n1.fa2.dy += yDist * factor;

						n2.fa2.dx -= xDist * factor;
						n2.fa2.dy -= yDist * factor;
					}
				}
			}
		},

		// Attraction force: Logarithmic, with Anti-Collision
		logAttraction_antiCollision : function(c) {
			this.coefficient = c;

			this.apply_nn = function(n1, n2, e) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;
					var distance = Math.sqrt(xDist * xDist + yDist * yDist);

					if(distance > 0) {
						// NB: factor = force / distance
						var factor = -this.coefficient * e * Math.log(1 + distance) / distance;

						n1.fa2.dx += xDist * factor;
						n1.fa2.dy += yDist * factor;

						n2.fa2.dx -= xDist * factor;
						n2.fa2.dy -= yDist * factor;
					}
				}
			}
		},

		// Attraction force: Linear, distributed by Degree, with Anti-Collision
		logAttraction_degreeDistributed_antiCollision : function(c) {
			this.coefficient = c;

			this.apply_nn = function(n1, n2, e) {
				if(n1.fa2 && n2.fa2) {
					// Get the distance
					var xDist = n1.x - n2.x;
					var yDist = n1.y - n2.y;
					var distance = Math.sqrt(xDist * xDist + yDist * yDist);

					if(distance > 0) {
						// NB: factor = force / distance
						var factor = -this.coefficient * e * Math.log(1 + distance) / distance / n1.fa2.mass;

						n1.fa2.dx += xDist * factor;
						n1.fa2.dy += yDist * factor;

						n2.fa2.dx -= xDist * factor;
						n2.fa2.dy -= yDist * factor;
					}
				}
			}
		}
	};
};

// The Region class, as used by the Barnes Hut optimization
sigma.forceatlas2.Region = function(nodes, depth) {
	sigma.classes.Cascade.call(this);
	this.depthLimit = 20;
	this.size = 0;
	this.nodes = nodes;
	this.subregions = [];
	this.depth = depth;

	this.p = {
		mass : 0,
		massCenterX : 0,
		massCenterY : 0
	};

	this.updateMassAndGeometry();
}

sigma.forceatlas2.Region.prototype.updateMassAndGeometry = function() {
	if(this.nodes.length > 1) {
		// Compute Mass
		var mass = 0;
		var massSumX = 0;
		var massSumY = 0;
		this.nodes.forEach(function(n) {
			mass += n.fa2.mass;
			massSumX += n.x * n.fa2.mass;
			massSumY += n.y * n.fa2.mass;
		});
		var massCenterX = massSumX / mass;
		massCenterY = massSumY / mass;

		// Compute size
		var size;
		this.nodes.forEach(function(n) {
			var distance = Math.sqrt((n.x - massCenterX) * (n.x - massCenterX) + (n.y - massCenterY) * (n.y - massCenterY));
			size = Math.max(size || (2 * distance), 2 * distance);
		});

		this.p.mass = mass;
		this.p.massCenterX = massCenterX;
		this.p.massCenterY = massCenterY;
		this.size = size;
	}
};

sigma.forceatlas2.Region.prototype.buildSubRegions = function() {
	if(this.nodes.length > 1) {
		var leftNodes = [];
		var rightNodes = [];
		var subregions = [];
		var massCenterX = this.p.massCenterX;
		var massCenterY = this.p.massCenterY;
		var nextDepth = this.depth + 1;

		var self = this;

		this.nodes.forEach(function(n) {
			var nodesColumn = (n.x < massCenterX) ? (leftNodes) : (rightNodes);
			nodesColumn.push(n);
		});

		var tl = [], bl = [], br = [], tr = [];

		leftNodes.forEach(function(n) {
			var nodesLine = (n.y < massCenterY) ? (tl) : (bl);
			nodesLine.push(n);
		});

		rightNodes.forEach(function(n) {
			var nodesLine = (n.y < massCenterY) ? (tr) : (br);
			nodesLine.push(n);
		});

		[tl, bl, br, tr].filter(function(a) {
			return a.length;
		}).forEach(function(a) {
			if(nextDepth <= self.depthLimit && a.length < self.nodes.length) {
				var subregion = new sigma.forceatlas2.Region(a, nextDepth);
				subregions.push(subregion);
			} else {
				a.forEach(function(n) {
					var oneNodeList = [n];
					var subregion = new sigma.forceatlas2.Region(oneNodeList, nextDepth);
					subregions.push(subregion);
				});
			}
		});

		this.subregions = subregions;

		subregions.forEach(function(subregion) {
			subregion.buildSubRegions();
		});
	}
};

sigma.forceatlas2.Region.prototype.applyForce = function(n, Force, theta) {
	if(this.nodes.length < 2) {
		var regionNode = this.nodes[0];
		Force.apply_nn(n, regionNode);
	} else {
		var distance = Math.sqrt((n.x - this.p.massCenterX) * (n.x - this.p.massCenterX) + (n.y - this.p.massCenterY) * (n.y - this.p.massCenterY));

		if(distance * theta > this.size) {
			Force.apply_nr(n, this);
		} else {
			this.subregions.forEach(function(subregion) {
				subregion.applyForce(n, Force, theta);
			});
		}
	}
};

sigma.publicPrototype.startForceAtlas2 = function() {
	//if(!this.forceatlas2) {
	this.forceatlas2 = new sigma.forceatlas2.ForceAtlas2(this._core.graph);
	this.forceatlas2.setAutoSettings();
	this.forceatlas2.init();
	//}

	this.addGenerator('forceatlas2', this.forceatlas2.atomicGo, function() {
		return true;
	});
};

sigma.publicPrototype.stopForceAtlas2 = function() {
	this.removeGenerator('forceatlas2');
};
